package com.clp.util.hutool.core.lang;

import com.clp.util.hutool.core.collection.CollUtil;
import com.clp.util.hutool.core.collection.EnumerationIter;
import com.clp.util.hutool.core.io.FileUtil;
import com.clp.util.hutool.core.io.IORuntimeException;
import com.clp.util.hutool.core.io.resource.ResourceUtil;
import com.clp.util.hutool.core.util.*;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class ClassScanner implements Serializable {

    private final String packageName;
    /**
     * 包名，最后跟一个点，表示包名，避免在检查前缀时的歧义
     */
    private final String packageNameWithDot;
    /**
     * 包路径，用于文件中对路径操作
     */
    private final String packageDirName;
    /**
     * 包路径，用于jar中对路径操作，在Linux下与packageDirName一致
     */
    private final String packagePath;
    /**
     * 过滤器
     */
    private final Filter<Class<?>> classFilter;
    /**
     * 编码
     */
    private final Charset charset;

    /**
     * 类加载器
     * @param packageName
     * @param classFilter
     */
    private ClassLoader classLoader;

    /**
     * 是否初始化类
     * @param packageName
     * @param classFilter
     */
    private boolean initialize;
    private final Set<Class<?>> classes = new HashSet<>();

    public ClassScanner(String packageName, Filter<Class<?>> classFilter) {
        this(packageName, classFilter, CharsetUtil.CHARSET_UTF_8);
    }

    public ClassScanner(String packageName, Filter<Class<?>> classFilter, Charset charset) {
        packageName = StrUtil.nullToEmpty(packageName);
        this.packageName = packageName;
        this.packageNameWithDot = StrUtil.addSuffixIfNot(packageName, StrUtil.DOT);
        this.packageDirName = packageName.replace(CharUtil.DOT, File.separatorChar);
        this.packagePath = packageName.replace(CharUtil.DOT, CharUtil.SLASH);
        this.classFilter = classFilter;
        this.charset = charset;
    }

    public Set<Class<?>> scan() {
        for (URL url : ResourceUtil.getResourceIter(this.packagePath)) {
            switch (url.getProtocol()) {
                case "file":
                    scanFile(new File(URLUtil.decode(url.getFile(), this.charset.name())), null);
                    break;
                case "jar":
                    scanJar(URLUtil.getJarFile(url));
                    break;
            }
        }

        if (CollUtil.isEmpty(this.classes)) {
            scanJavaClassPaths();
        }

        return Collections.unmodifiableSet(this.classes);
    }

    /**
     * 扫描文件或目录中的类
     * @param file
     * @param rootDir
     */
    private void scanFile(File file, String rootDir) {
        if (file.isFile()) {
            final String filename = file.getAbsolutePath();
            if (filename.endsWith(FileUtil.CLASS_EXT)) {
                final String className = filename
                        // 8 为 classes 长度，fileName.length() - 6 为 ".class"的长度
                        .substring(rootDir.length(), filename.length() - 6)
                        .replace(File.separatorChar, CharUtil.DOT);
                // 加入满足条件的类
                addIfAccept(className);
            } else if (filename.endsWith(FileUtil.JAR_FILE_EXT)) {
                try {
                    scanJar(new JarFile(file));
                } catch (IOException e) {
                    throw new IORuntimeException(e);
                }
            }
        } else if (file.isDirectory()) {
            final File[] files = file.listFiles();
            if (null != files) {
                for (File subFile : files) {
                    scanFile(subFile, (null == rootDir) ? subPathBeforePackage(file) : rootDir);
                }
            }
        }
    }

    /**
     * 通过过滤器，是否满足此类的条件
     * @param className
     */
    private void addIfAccept(String className) {
        if (StrUtil.isBlank(className)) {
            return;
        }
        int classLen = className.length();
        int packageLen = this.packageName.length();
        if (classLen == packageLen) {
            // 类名和包名长度一致，用户可能传入的包名是类名
            if (className.equals(this.packageName)) {
                addIfAccept(loadClass(className));
            }
        } else if (classLen > packageLen) {
            // 检查类型是否以指定包名为前缀，包名后加.
            // 避免类似于cn.hutool.A 和 cn.hutool.ATest 这类类名引起的歧义
            if (className.startsWith(this.packageNameWithDot)) {
                addIfAccept(loadClass(className));
            }
        }
    }

    private void addIfAccept(Class<?> clazz) {
        if (null != clazz) {
            Filter<Class<?>> classFilter = this.classFilter;
            if (classFilter == null || classFilter.accept(clazz)) {
                this.classes.add(clazz);
            }
        }
    }

    private Class<?> loadClass(String className) {
        ClassLoader loader = this.classLoader;
        if (null == loader) {
            loader = ClassLoaderUtil.getClassLoader();
            this.classLoader = loader;
        }

        Class<?> clazz = null;
        try {
            clazz = Class.forName(className, this.initialize, loader);
        } catch (NoClassDefFoundError e) {
            // 由于依赖库导致的类无法加载，直接跳过此类
        } catch (UnsupportedClassVersionError e) {
            // 版本导致的不兼容的类，跳过
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return clazz;
    }

    /**
     * 扫描 jar 包
     * @param jar
     */
    private void scanJar(JarFile jar) {
        String name;
        for (JarEntry entry : new EnumerationIter<>(jar.entries())) {
            name = StrUtil.removePrefix(entry.getName(), StrUtil.SLASH);
            if (name.startsWith(this.packagePath)) {
                if (name.endsWith(FileUtil.CLASS_EXT) && !entry.isDirectory()) {
                    final String className = name
                            .substring(0, name.length() - 6)
                            .replace(CharUtil.SLASH, CharUtil.DOT);
                    addIfAccept(loadClass(className));
                }
            }
        }
    }

    /**
     * 截取文件绝对路径中包名之前的部分
     * @param file
     * @return
     */
    private String subPathBeforePackage(File file) {
        String filePath = file.getAbsolutePath();
        if (StrUtil.isNotEmpty(this.packageDirName)) {
            filePath = StrUtil.subBefore(filePath, this.packageDirName, true);
        }
        return StrUtil.addSuffixIfNot(filePath, File.separator);
    }

    /**
     * 扫描Java指定的ClassPath路径
     */
    private void scanJavaClassPaths() {
        final String[] javaClassPaths = ClassUtil.getJavaClassPaths();
        for (String classPath : javaClassPaths) {
            // bug修复：由于路径中空格和中文导致的Jar找不到
            classPath = URLUtil.decode(classPath, CharsetUtil.systemCharsetName());

            scanFile(new File(classPath), null);
        }
    }

    public static Set<Class<?>> scanPackage(String packageName, Filter<Class<?>> classFilter) {
        return new ClassScanner(packageName, classFilter).scan();
    }
}
